/*  
 *  Copyright 2006 Paul Jack.
 *
 *  This file is part of the Dex compiler suite. 
 *  
 *  Dex is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License as published by the
 *  Free Software Foundation; either version 2 of the License, or (at your
 *  option) any later version.
 *  
 *  Dex is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 *  Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */
package dex.compiler.checker;


import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import dex.compiler.checker.type.ArrayType;
import dex.compiler.checker.type.BooleanType;
import dex.compiler.checker.type.ErrorType;
import dex.compiler.checker.type.FunctionType;
import dex.compiler.checker.type.IntegerType;
import dex.compiler.checker.type.MemberType;
import dex.compiler.checker.type.NullType;
import dex.compiler.checker.type.Type;
import dex.compiler.checker.type.TypeManager;
import dex.compiler.checker.type.TypeProblems;
import dex.compiler.model.base.Identifiers;
import dex.compiler.model.base.Place;
import dex.compiler.model.definition.ClassDef;
import dex.compiler.model.definition.Constructor;
import dex.compiler.model.definition.Function;
import dex.compiler.model.definition.TypedName;
import dex.compiler.model.definition.Unit;
import dex.compiler.model.expression.ArrayExpression;
import dex.compiler.model.expression.CastExpression;
import dex.compiler.model.expression.DefaultExpressionVisitor;
import dex.compiler.model.expression.DirectCall;
import dex.compiler.model.expression.IdentifierExpression;
import dex.compiler.model.expression.IndirectCall;
import dex.compiler.model.expression.InfixExpression;
import dex.compiler.model.expression.IsExpression;
import dex.compiler.model.expression.LiteralBoolean;
import dex.compiler.model.expression.LiteralFunction;
import dex.compiler.model.expression.LiteralInteger;
import dex.compiler.model.expression.LiteralString;
import dex.compiler.model.expression.MemberExpression;
import dex.compiler.model.expression.NewExpression;
import dex.compiler.model.expression.UnaryExpression;
import dex.compiler.model.expression.VirtualCall;
import dex.compiler.model.program.NameSearcher;
import dex.compiler.model.program.Path;
import dex.compiler.model.program.Program;
import dex.compiler.model.program.Programs;
import dex.compiler.problem.Reporter;
import dex.misc.Strings;

import static dex.compiler.checker.ExpressionProblems.*;


/**
 * Checks that expressions are vaild.  
 * 
 * <p>This visitor ensures that all referenced functions, variables,
 * classes, records, methods and fields all exist, are not ambiguous,
 * and are visible in terms of access modifiers. 
 * 
 * <p>Also, the evaluation types of the expressions are determined
 * and validated, so that <code>3 &gt; "4"</code> generates an error.  
 */
public class ExpressionChecker extends DefaultExpressionVisitor {

	
	/**
	 * The error type.  This is generated as an expression's evaluation
	 * type in the event of an error.
	 */
	final static Type ERROR = ErrorType.INSTANCE;


	/**
	 *  Scope for resolving local variables.
	 */
	final Scope scope;


	/**
	 * Stack of expression evaluation types.
	 */
	final private LinkedList<Type> stack;


	/**
	 * Flag indicating if the most recent visitation generated
	 * an error.
	 */
	private boolean errorFlag;


	/**
	 * Manager for types.
	 */
	final TypeManager manager;


	/**
	 * The program being checked.
	 */
	final Program program;
	
	
	/**
	 * The specific unit (source file) being checked.
	 */
	final Unit unit;
	
	
	/**
	 * The reporter used to report errors or warnings.
	 */
	final Reporter reporter;
	
	
	/**
	 * Constructs a new ExpressionChecker.
	 * 
	 * @param config  the configuration for the checker
	 */
	public ExpressionChecker(CheckerConfig config) {
		config.validate();
		this.stack = new LinkedList<Type>();
		this.manager = new TypeManager(config.getProgram(), config.getReporter());
		this.program = config.getProgram();
		this.unit = config.getUnit();
		this.reporter = config.getReporter();
		this.scope = new Scope();
	}
	
	
	/**
	 * Pushes an evaluation type onto the stack.
	 * 
	 * @param type  the type to push
	 */
	void push(Type type) {
		stack.addLast(type);
	}
	
	
	/**
	 * Pops an evaluation type from the stack.
	 * If the popped type is the error type,
	 * then <code>errorFlag</code> is set.
	 * 
	 * @return  the popped evaluation type
	 */
	protected Type pop() {
		Type result = stack.removeLast();
		if (result == ERROR) {
			errorFlag = true;
		}
		return result;
	}

	
	/**
	 * Checks for an error.  If <code>errorFlag</code> is true
	 * (which will only happen if <code>pop()</code> returns
	 * the error type) then the error flag is cleared, and 
	 * the error type is pushed.  
	 * 
	 * <p>All expressions that have one or more
	 * subexpressions should invoke flag() to see if any of the 
	 * subexpressions were invalid.  If flag() returns true, then
	 * the visit method should return without validating, since
	 * if input subexpressions were invalid then the output of
	 * the visited expression will also be invalid.  Since 
	 * <code>flag()</code> pushes the error type if <code>pop()</code>
	 * returned it, this convention ensures that error status
	 * is propogated through a complex expression without reporting
	 * many redundant errors.  
	 * 
	 * @return  true if an error was detected, false otherwise
	 */
	protected boolean flag() {
		if (errorFlag) {
			errorFlag = false;
			push(ERROR);
			return true;
		}
		return false;
	}


	/**
	 * Reports the given error, and pushes the error type.
	 * 
	 * @param place  the place where the error occurred
	 * @param msg    the message to report.
	 */
	//private void error(Place place, String msg) {
	//	reporter.error(place, msg);
	//	push(ERROR);
	//}


	/**
	 * Pops a list of evaluation types.  The list is popped in
	 * reverse order; result.get(0) is the least recently popped 
	 * elements.  In other words, expression lists are evaluated
	 * in the order they appear. 
	 * 
	 * @param size  the size of the list to pop
	 * @return      the list of evaluation types
	 */
	List<Type> pop(int size) {
		Type[] result = new Type[size];
		for (int i = size - 1; i >= 0; i--) {
			result[i] = pop();
		}
		return Arrays.asList(result);
	}


	/**
	 * Checks an array expression for validity.
	 * 
	 * @param e  the array expression to check
	 */
	@Override
	public void visitArrayExpression(ArrayExpression e) {
		Type indexType = pop();
		Type arrayType = pop();
		if (flag()) return;
		
		if (!(arrayType instanceof ArrayType)) {
			reporter.error(BAD_BRACKETS, e.getPlace(), arrayType.toString());
			push(ERROR);
			return;
		}
		if (!(indexType instanceof IntegerType)) {
			reporter.error(BAD_INDEX, e.getPlace(), indexType.toString());
			push(ERROR);
			return;
		}
		ArrayType at = (ArrayType)arrayType;
		push(at.getElementType());
	}


	/**
	 * Checks a cast expression for validity.
	 * 
	 * @param e  the cast expression to check
	 */
	@Override
	public void visitCastExpression(CastExpression e) {
		Type input = pop();
		if (flag()) return;
		
		Type resultType = manager.toType(unit, e.getType());
		if (!input.canCastTo(resultType)) {
			reporter.error(ILLEGAL_CAST, e.getPlace(), input.toString(), resultType.toString());
			push(ERROR);
		} else {
			push(resultType);			
		}
	}


	/**
	 * Checks a direct call expression for validity.
	 * 
	 * @param e  the direct call expression to check
	 */
	@Override
	public void visitDirectCall(DirectCall e) {
		List<Type> arguments = pop(e.getParameters().size());

		FunctionSearcher searcher = new FunctionSearcher(Function.class, manager, e.getName(), arguments);
		List<Path> found = Programs.search(program, unit, searcher);
		if (found.size() == 0) {
			reporter.error(MISSING_FUNCTION, e.getPlace(), e.getName()); // FIXME: Include argument types
			push(ERROR);
			return;
		}
		if (found.size() > 1) {
			reporter.error(AMBIGUOUS_FUNCTION, e.getPlace(), e.getName(), found); // FIXME: Include argument types
			push(ERROR);
			return;
		}
		
		Path path = found.get(0);
		if (!Programs.checkVisibility(unit, path)) {
			reporter.error(INVISIBLE_FUNCTION, e.getPlace(), e.getName());
			push(ERROR);
			return;
		}
		
		Function f = (Function)path.getDefinition();
		push(manager.toType(path.getUnit(), f.getReturnType()));
	}

	
	/**
	 * Checks an identifier expression for validity.
	 * 
	 * @param e  the identifier expression to check
	 */
	@Override
	public void visitIdentifierExpression(IdentifierExpression e) {
		String name = e.getIdentifier();

		// Check keyword literals first.
		if (e.getIdentifier().equals("true")) {
			push(BooleanType.INSTANCE);
			return;
		}
		if (e.getIdentifier().equals("false")) {
			push(BooleanType.INSTANCE);
			return;
		}
		if (e.getIdentifier().equals("null")) {
			push(NullType.INSTANCE);
			return;
		}

		// Check local variables next.
		if (!Identifiers.isQualified(name)) {
			Type type = scope.getType(name);
			if (type != null) {
				push(type);
				return;
			}
		}

		// Search for global variables.
		NameSearcher searcher = new NameSearcher(TypedName.class, name);
		List<Path> found = Programs.search(program, unit, searcher);
		if (found.size() == 0) {
			reporter.error(MISSING_VARIABLE, e.getPlace(), name);
			push(ERROR);
			return;
		}
		if (found.size() > 1) {
			reporter.error(AMBIGUOUS_VARIABLE, e.getPlace(), name, found);
			push(ERROR);
			return;
		}
		
		Path path = found.get(0);
		if (!Programs.checkVisibility(unit, path)) {
			reporter.error(INVISIBLE_VARIABLE, e.getPlace(), path.qualified());
			push(ERROR);
			return;
		}
		
		TypedName global = (TypedName)found.get(0).getDefinition();
		push(manager.toType(path.getUnit(), global.getType()));
	}


	/**
	 * Checks an indirect call expression for validity.
	 * 
	 * @param e  the indirect call expression to test
	 */
	@Override
	public void visitCallExpression(IndirectCall e) {
		List<Type> arguments = pop(e.getParameters().size());
		Type ownerType = pop();
		if (flag()) return;
		
		if (!(ownerType instanceof FunctionType)) {
			reporter.error(BAD_CALL, e.getPlace(), ownerType.toString());
			push(ERROR);
			return;
		}
		
		FunctionType ft = (FunctionType)ownerType;
		List<Type> formal = ft.getParameterTypes();
		if (!Type.canAssignTo(arguments, formal)) {
			reporter.error(BAD_ARGUMENTS, e.getPlace(), Strings.list(arguments), Strings.list(formal));
			push(ERROR);
			return;
		}
		
		push(ft.getReturnType());
	}

	
	/**
	 * Check an infix expression for validity.
	 * 
	 * @param e  the infix expression to check
	 */
	@Override
	public void visitInfixExpression(InfixExpression e) {
		Type left = pop();
		Type right = pop();
		if (flag()) return;
		
		InfixRules rules = new InfixRules(reporter, e.getPlace(), e.getOperator(), left, right);
		push(rules.check());
	}


	/**
	 * Checks a literal boolean expression validity.
	 * 
	 * @param e  the literal boolean expression to check 
	 */
	@Override
	public void visitLiteralBoolean(LiteralBoolean e) {
		push(BooleanType.INSTANCE);
	}


	/**
	 * Checks a literal function expression for validity.
	 * 
	 * @param e  the literal function expression to check
	 */
	@Override
	public void visitLiteralFunction(LiteralFunction e) {
		List<Type> ptypes = manager.toTypes(unit, e.getParameterTypes());
		FunctionSearcher searcher = new FunctionSearcher(Function.class, manager, e.getName(), ptypes);
		List<Path> found = Programs.search(program, unit, searcher);
		if (found.size() == 0) {
			reporter.error(MISSING_FUNCTION, e.getPlace(), e.getName());
			push(ERROR);
			return;
		}
		if (found.size() > 1) {
			reporter.error(AMBIGUOUS_FUNCTION, e.getPlace(), e.getName(), found);
			push(ERROR);
			return;
		}
		
		Path path = found.get(0);
		if (!Programs.checkVisibility(unit, path)) {
			reporter.error(INVISIBLE_FUNCTION, e.getPlace(), path.qualified());
			push(ERROR);
			return;
		}
		Function f = (Function)path.getDefinition();
		ptypes.clear();
		for (TypedName p: f.getParameters()) {
			ptypes.add(manager.toType(path.getUnit(), p.getType()));
		}
		Type returnType = manager.toType(path.getUnit(), f.getReturnType());
		push(manager.getFunctionType(returnType, ptypes));
	}


	/**
	 * Checks a literal integer expression for validity.
	 * 
	 * @param e  the literal integer expression to check
	 */
	@Override
	public void visitLiteralInteger(LiteralInteger e) {
		int width = IntegerType.getMinimumBitWidth(false, e.getValue());
		push(IntegerType.get(false, width));
	}


	/**
	 * Checks a literal string expression for validity.
	 * 
	 * @param e  the literal string expression to check
	 */
	@Override
	public void visitLiteralString(LiteralString e) {
		push(manager.getStringType());
	}


	/**
	 * Checks a unary operator expression for validity.
	 * 
	 * @param e  the unary operator expression to check
	 */
	@Override
	public void visitUnaryExpression(UnaryExpression e) {
		Type operand = pop();
		if (flag()) return;
		UnaryRules rules = new UnaryRules(reporter, e.getPlace(), e.getOperator(), operand);
		push(rules.check());
	}


	/**
	 * Checks a member expression for validity.
	 * 
	 * @param e  the member expression to check
	 */
	@Override
	public void visitMemberExpression(MemberExpression e) {
		Type ownerType = pop();
		if (flag()) return;
		
		if (!(ownerType instanceof MemberType)) {
			reporter.error(BAD_DOT, e.getPlace(), ownerType.toString());
			push(ERROR);
			return;
		}

		if (Identifiers.isQualified(e.getMemberName())) {
			reporter.error(BAD_FIELD_NAME, e.getPlace(), e.getMemberName());
			push(ERROR);
			return;
		}

		MemberType mt = (MemberType)ownerType;
		Path def = manager.getDefinition(mt);
		NameSearcher searcher = new NameSearcher(TypedName.class, e.getMemberName());
		List<Path> found = manager.search(def, searcher);
		if (found.size() == 0) {
			reporter.error(MISSING_MEMBER, e.getPlace(), e.getMemberName());
			push(ERROR);
			return;
		}
		if (found.size() > 1) {
			// This is a problem, but it will be reported when the class is analyzed
			// by DefinitionChecker.
			// reporter.error(AMBIGUOUS_MEMBER, ut.getPlace(), e.getMemberName(), found);
			push(ERROR);
			return;
		}
		
		Path path = found.get(0);
		if (!Programs.checkVisibility(unit, path)) {
			reporter.error(INVISIBLE_FIELD, e.getPlace(), path.qualified());
			push(ERROR);
			return;
		}
		Path classPath = new Path(path.getProgram(), path.getUnit(), path.getUnit(), path.getContainer());
		if (!Programs.checkVisibility(unit, classPath)) {
			reporter.error(TypeProblems.INVISIBLE_TYPE, e.getPlace(), classPath.qualified());
			push(ERROR);
		}
		TypedName tn = (TypedName)path.getDefinition();
		push(manager.toType(path.getUnit(), tn.getType()));
	}


	/**
	 * Checks a virtual call expression to check.
	 * 
	 * @param e  the virtual call expression to check
	 */
	@Override
	public void visitVirtualCall(VirtualCall e) {
		List<Type> ptypes = pop(e.getParameters().size());
		Type ownerType = pop();
		if (flag()) return;
		
		if (!(ownerType instanceof MemberType)) {
			reporter.error(BAD_DOT, e.getPlace(), ownerType.toString());
			push(ERROR);
			return;
		}

		if (Identifiers.isQualified(e.getName())) {
			reporter.error(BAD_METHOD_NAME, e.getPlace(), e.getName());
			push(ERROR);
			return;
		}

		MemberType mt = (MemberType)ownerType;
		Path def = manager.getDefinition(mt);
		if (!(def.getDefinition() instanceof ClassDef)) {
			reporter.error(RECORD_INVOCATION, e.getPlace(), def.qualified());
			push(ERROR);
			return;
		}
		
		FunctionSearcher searcher = new FunctionSearcher(Function.class, manager, e.getName(), ptypes);
		List<Path> found = manager.search(def, searcher);
		if (found.size() == 0) {
			reporter.error(MISSING_METHOD, e.getPlace(), e.getName()); // FIXME: Arguments
			push(ERROR);
			return;
		}
		if (found.size() > 1) {
			reporter.error(AMBIGUOUS_METHOD, e.getPlace(), e.getName(), found); // FIXME: Arguments
			push(ERROR);
			return;
		}
		
		Path path = found.get(0);
		if (!Programs.checkVisibility(unit, path)) {
			reporter.error(INVISIBLE_METHOD, e.getPlace(), path.qualified());
			push(ERROR);
			return;
		}
		Path classPath = new Path(path.getProgram(), path.getUnit(), path.getUnit(), path.getContainer());
		if (!Programs.checkVisibility(unit, classPath)) {
			reporter.error(TypeProblems.INVISIBLE_TYPE, e.getPlace(), classPath.qualified());
			push(ERROR);
		}

		Function f = (Function)path.getDefinition();
		Type returnType = manager.toType(path.getUnit(), f.getReturnType());
		push(returnType);
	}


	public void visitNewExpression(NewExpression e) {
		List<Type> ptypes = pop(e.getParameters().size());
		if (flag()) return;

		Type type = manager.toType(unit, e.getType());
		if (type instanceof ArrayType) {
			visitNewArray((ArrayType)type, e.getPlace(), ptypes);
		} else if (type instanceof MemberType) {
			MemberType mt = (MemberType)type;
			if (mt.isRecord()) {
				visitNewRecord(mt, e.getPlace(), ptypes);
			} else {
				visitNewClass(mt, e.getPlace(), ptypes);
			}
		} else {
			reporter.error(BAD_NEW, e.getPlace(), type.toString());
			push(ERROR);
		}
	}


	private void visitNewArray(ArrayType array, Place place, List<Type> ptypes) {
		boolean error = false;
		if (ptypes.isEmpty()) {
			reporter.error(BAD_ARRAY_NEW, place, "");
			push(ERROR);
			return;
		}

		for (Type t: ptypes) {
			if (!(t instanceof IntegerType)) {
				reporter.error(BAD_ARRAY_BOUND, place, t.toString());
				error = true;
			}
		}
		if (error) {
			push(ERROR);
		} else {
			push(array);
		}
	}


	private void visitNewRecord(MemberType record, Place place, List<Type> ptypes) {
		
	}


	private void visitNewClass(MemberType cls, Place place, List<Type> ptypes) {
		Path def = manager.getDefinition(cls);
		if (!(def.getDefinition() instanceof ClassDef)) {
			reporter.error(RECORD_INVOCATION, place, def.qualified());
			push(ERROR);
			return;
		}
		
		FunctionSearcher searcher = new FunctionSearcher(Constructor.class, manager, "new", ptypes);
		List<Path> found = Programs.search(program, def.getUnit(), (ClassDef)def.getDefinition(), searcher);
		if (found.size() == 0) {
			reporter.error(MISSING_CONSTRUCTOR, place, cls.getName()); // FIXME: Arguments
			push(ERROR);
			return;
		}
		if (found.size() > 1) {
			reporter.error(AMBIGUOUS_CONSTRUCTOR, place, cls.getName(), found); // FIXME: Arguments
			push(ERROR);
			return;
		}
		
		Path path = found.get(0);
		if (!Programs.checkVisibility(unit, path)) {
			reporter.error(INVISIBLE_CONSTRUCTOR, place, path.qualified());
			push(ERROR);
			return;
		}
		if (!Programs.checkVisibility(unit, def)) {
			reporter.error(TypeProblems.INVISIBLE_TYPE, place, def.qualified());
			push(ERROR);
			return;
		}
		
		push(cls);
	}

	
	public void visitIsExpression(IsExpression e) {
		Type actual = pop();
		if (flag()) return;
		
		if (!(actual instanceof MemberType)) {
			reporter.error(BAD_IS_EXP, e.getPlace(), actual.toString());
			push(ERROR);
			return;
		}

		Type formal = manager.toType(unit, e.getType());

		if (!(formal instanceof MemberType)) {
			reporter.error(BAD_IS_TYPE, e.getPlace(), formal.toString());
			push(ERROR);
			return;
		}
		
		push(BooleanType.INSTANCE);
	}

}
